﻿//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  DispatcherProxyTest.cs
//    
// Abstract:
//
//  Unit tests for the DispactherProxy class.
//  
//--

using System;
using System.Diagnostics;
using System.Messaging;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using TestHelpers = Microsoft.EnhancedReminders.UnitTests.Helpers;

using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.Scheduler.UnitTests
{
    /// <summary>
    /// This is a test class for DispatcherProxy and is intended
    /// to contain all DispatcherProxy Unit Tests
    /// </summary>
    [TestClass()]
    public class DispatcherProxyTest
    {
        // Message queue paths for the DispatcherProxy
        private const string _sendQueuePath = @".\Private$\EnhancedRemindersNotificationsUnitTest";
        private const string _responseQueuePath = @".\Private$\EnhancedRemindersResponsesUnitTest";
        private const string _administrationQueuePath = @".\Private$\EnhancedRemindersAdministrationQueueUnitTest";

        // Message queues 
        private static MessageQueue _sendQueue;
        private static MessageQueue _responseQueue;
        private static MessageQueue _administrationQueue;

        // Specifies the cut-off period after which the DispatcherProxy will not send a message
        private TimeSpan _cutOffPeriod = TimeSpan.FromMinutes(2);

        // The event args passed to the NotificationCompleted event handler
        private NotificationCompletedEventArgs _actualEventArgs;

        // Used to wait for the NotificationCompleted event to be raised
        private static ManualResetEvent _waitEvent;

        // The DispatcherProxy instance being tested
        private static DispatcherProxy _target;

        /// <summary>
        /// Create the queues and wait event.
        /// </summary>
        /// <param name="testContext">Not used</param>
        /// <remarks>
        /// The unit test creates the MSMQs used by the DispatcherProxy so that the test is not dependent on setup.
        /// </remarks>
        [ClassInitialize()]
        public static void MyClassInitialize(TestContext testContext)
        {
            _waitEvent = new ManualResetEvent(false);
            _sendQueue = CreateQueue(_sendQueuePath);
            _responseQueue = CreateQueue(_responseQueuePath);
            _administrationQueue = CreateQueue(_administrationQueuePath);
        }

        /// <summary>
        /// Delete the queues and wait event.
        /// </summary>
        [ClassCleanup()]
        public static void MyClassCleanup()
        {
            _waitEvent.Close();
            MessageQueue.Delete(_sendQueuePath);
            MessageQueue.Delete(_responseQueuePath);
            MessageQueue.Delete(_administrationQueuePath);
        }

        /// <summary>
        /// Reset the queues and wait event, and create a new DispactherProxy instance for each test.
        /// </summary>
        [TestInitialize()]
        public void MyTestInitialize()
        {
            Debug.Assert(_sendQueue != null && _responseQueue != null && _administrationQueue != null, "Queues not initialized");
            _sendQueue.Purge();
            _responseQueue.Purge();
            _administrationQueue.Purge();
            _waitEvent.Reset();
            _actualEventArgs = null;
            _target = new DispatcherProxy(_sendQueuePath, _responseQueuePath, _administrationQueuePath, _cutOffPeriod);
        }

        /// <summary>
        /// Dispose of the DispatcherProxy instance used by the proceeding test.
        /// </summary>
        [TestCleanup()]
        public void MyTestCleanup()
        {
            if (_target != null)
            {
                _target.Dispose();
                _target = null;
            }
        }
        
        /// <summary>
        /// Test that a current notification is enqueued when SendNotification is called.
        /// </summary>
        /// <remarks>
        /// This test passes a notification to SendNotification that has a reminder time that matches the current time.
        /// The test verifies that this message is put in the send queue.
        /// </remarks>
        [TestMethod()]
        public void SendNotificationTest()
        {
            DateTime expectedTime = DateTime.UtcNow;
            NotificationItem expectedItem = TestHelpers.CreateNotificationItem("1", expectedTime);
            _target.SendNotification(expectedItem);
            Message message = _sendQueue.Receive(TimeSpan.Zero);
            Assert.IsNotNull(message, "Send queue should have one entry");
            message.Formatter = new BinaryMessageFormatter();
            NotificationItem actualItem = message.Body as NotificationItem;
            Assert.IsNotNull(actualItem, "Message was in the wrong format");
            Assert.AreEqual(expectedItem.Id.Id, actualItem.Id.Id, "Unexpected item in send queue");
            VerifyAllQueuesAreEmpty();
        }

        /// <summary>
        /// Test that an out-of-date notification is not enqueued when SendNotification is called.
        /// </summary>
        /// <remarks>
        /// This test passes a notification to SendNotification that has a reminder time that is beyond the cut-off period 
        /// for sending. The test verifies that the message is not put in the send queue and that the NotificationCompleted 
        /// event is raised with the correct event args.
        /// </remarks>
        [TestMethod()]
        public void SendNotificationTooLateTest()
        {
            DateTime expectedTime = DateTime.UtcNow - (_cutOffPeriod + _cutOffPeriod);
            NotificationItem expectedItem = TestHelpers.CreateNotificationItem("1", expectedTime);
            _target.NotificationCompleted += DispatcherProxy_NotificationCompleted;
            _target.SendNotification(expectedItem);
            _target.NotificationCompleted -= DispatcherProxy_NotificationCompleted;
            VerifyEventArgs(_actualEventArgs, expectedItem.Id.Id, NotificationResultStatus.Error, NotificationChannelType.None);
            VerifyAllQueuesAreEmpty();
        }

        /// <summary>
        /// Test that a disbaled notification is not enqueued when SendNotification is called.
        /// </summary>
        /// <remarks>
        /// This test passes a notification to SendNotification that has no notification channels. The test verifies that the 
        /// message is not put in the send queue and that the NotificationCompleted event is raised with the correct event args.
        /// </remarks>
        [TestMethod()]
        public void SendNotificationDisabledTest()
        {
            DateTime expectedTime = DateTime.UtcNow;
            NotificationItem expectedItem = TestHelpers.CreateDisabledNotificationItem("1", expectedTime);
            _target.NotificationCompleted += DispatcherProxy_NotificationCompleted;
            _target.SendNotification(expectedItem);
            _target.NotificationCompleted -= DispatcherProxy_NotificationCompleted;
            VerifyEventArgs(_actualEventArgs, expectedItem.Id.Id, NotificationResultStatus.Error, NotificationChannelType.None);
            VerifyAllQueuesAreEmpty();
        }

        /// <summary>
        /// Test that a message expires when it is not read from the send queue in time.
        /// </summary>
        /// <remarks>
        /// This test passes a notification to SendNotification that is close to expiring. The test then verifies that the 
        /// message expires and the NotificationCompleted event is raised with the correct event args.
        /// </remarks>
        [TestMethod()]
        public void SendNotificationMessageTimeoutTest()
        {
            _target.Open();
            int messageTimeoutMs = 3000;
            DateTime expectedTime = DateTime.UtcNow - (_cutOffPeriod - TimeSpan.FromMilliseconds(messageTimeoutMs));
            NotificationItem expectedItem = TestHelpers.CreateNotificationItem("1", expectedTime);
            _target.NotificationCompleted += DispatcherProxy_NotificationCompleted;
            _target.SendNotification(expectedItem);
            Assert.IsTrue(_sendQueue.GetAllMessages().Length == 1, "Send queue should only have one entry");
            Assert.IsNull(_actualEventArgs, "NotificationCompleted raised too soon");
            TestHelpers.WaitOne(_waitEvent, messageTimeoutMs * 2, "NotificationCompleted not raised");
            _target.NotificationCompleted -= DispatcherProxy_NotificationCompleted;
            VerifyEventArgs(_actualEventArgs, expectedItem.Id.Id, NotificationResultStatus.Error, NotificationChannelType.None);
            VerifyAllQueuesAreEmpty();
        }

        /// <summary>
        /// Test that response messages are handled correctly.
        /// </summary>
        /// <remarks>
        /// The test sends a message to the response queue and verifies that this is received by the DispatcherProxy and 
        /// the NotificationCompleted event is raised with the correct event args.
        /// </remarks>
        [TestMethod()]
        public void ResponseMessageTest()
        {
            _target.Open();
            NotificationResult expectedResult = new NotificationResult(Context.Global, new NotificationId("1", "john@contoso.com"), NotificationResultStatus.Success, NotificationChannelType.Communicator);
            Message message = new Message(expectedResult, new BinaryMessageFormatter());
            _target.NotificationCompleted += DispatcherProxy_NotificationCompleted;
            _responseQueue.Send(message);
            TestHelpers.WaitOne(_waitEvent, "NotificationCompleted not raised");
            _target.NotificationCompleted -= DispatcherProxy_NotificationCompleted;
            VerifyEventArgs(_actualEventArgs, expectedResult.Id.Id, NotificationResultStatus.Success, NotificationChannelType.Communicator);
            VerifyAllQueuesAreEmpty();
        }

        /// <summary>
        /// Test that response messages are handled correctly even when they are enqueued before the DispatcherProxy 
        /// has started receiving.
        /// </summary>
        /// <remarks>
        /// This test sends a message to the response queue before the DispatcherProxy has started receiving messages 
        /// from the queue. The test then verifies that when the DispatcherProxy starts receiving messages it receives 
        /// the previously enqueued message and raises the NotificationCompleted event with the correct event args.
        /// </remarks>
        [TestMethod()]
        public void ResponseMessageBeforeOpenTest()
        {
            NotificationResult expectedResult = new NotificationResult(Context.Global, new NotificationId("1", "john@contoso.com"), NotificationResultStatus.Success, NotificationChannelType.Communicator);
            Message message = new Message(expectedResult, new BinaryMessageFormatter());
            _target.NotificationCompleted += DispatcherProxy_NotificationCompleted;
            _responseQueue.Send(message);
            bool eventRaised = _waitEvent.WaitOne(1000, false);
            Assert.IsFalse(eventRaised, "NotificationCompleted was unexpectedly raised");
            _target.Open();
            TestHelpers.WaitOne(_waitEvent, "NotificationCompleted not raised");
            _target.NotificationCompleted -= DispatcherProxy_NotificationCompleted;
            VerifyEventArgs(_actualEventArgs, expectedResult.Id.Id, NotificationResultStatus.Success, NotificationChannelType.Communicator);
            VerifyAllQueuesAreEmpty();
        }

        /// <summary>
        /// Creates a queue with the specified path. 
        /// </summary>
        /// <param name="path">Path for queue</param>
        /// <returns>
        /// The requested queue. If the queue already exists, the existing queue is returned.
        /// </returns>
        private static MessageQueue CreateQueue(string path)
        {
            MessageQueue queue = null;

            try
            {
                // Create the specified queue
                queue = MessageQueue.Create(path);
            }
            catch (MessageQueueException ex)
            {
                if (ex.MessageQueueErrorCode != MessageQueueErrorCode.QueueExists)
                {
                    // Failed to create queue
                    throw;
                }

                // The queue already exists. This might be the case if these unit tests did not exit cleanly the 
                // previous time they were run.
                queue = new MessageQueue(path);
            }

            return queue;
        }

        /// <summary>
        /// Handles confirmation that a notifictaion has been delivered, successfully or otherwise.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Identifies the result of the notification</param>
        private void DispatcherProxy_NotificationCompleted(object sender, NotificationCompletedEventArgs e)
        {
            _actualEventArgs = e;
            _waitEvent.Set();
        }

        /// <summary>
        /// Verifies that all three queues used by the DispatcherProxy are empty.
        /// </summary>
        private static void VerifyAllQueuesAreEmpty()
        {
            Assert.IsTrue(_sendQueue.GetAllMessages().Length == 0, "Send queue should be empty");
            Assert.IsTrue(_responseQueue.GetAllMessages().Length == 0, "Response queue should be empty");
            Assert.IsTrue(_administrationQueue.GetAllMessages().Length == 0, "Administration queue should be empty");
        }

        /// <summary>
        /// Verifies that the event args passed to the NotificationCompleted event handler are as expected.
        /// </summary>
        /// <param name="e">Event args</param>
        /// <param name="expectedId">Expected notification ID</param>
        /// <param name="expectedStatus">Expected notification completion status</param>
        /// <param name="channelType">Expected successful notification channel</param>
        private static void VerifyEventArgs(NotificationCompletedEventArgs e, string expectedId, 
            NotificationResultStatus expectedStatus, NotificationChannelType channelType)
        {
            Assert.IsNotNull(e, "NotificationCompleted not raised");
            Assert.IsNotNull(e.Result, "NotificationCompletedEventArgs.Result should not be null");
            Assert.IsNotNull(e.Result.Id, "NotificationCompletedEventArgs.Result.Id should not be null"); 
            Assert.IsTrue(e.Result.Id is NotificationId, "NotificationCompletedEventArgs.Result.Id should be an NotificationId type");
            Assert.AreEqual(expectedId, e.Result.Id.Id, "NotificationCompleted raised with wrong ID");
            Assert.AreEqual(expectedStatus, e.Result.Status, "NotificationCompleted raised with wrong status");
            Assert.AreEqual(channelType, e.Result.HowContacted, "NotificationCompleted raised with wrong channel");
        }
    }
}
